Skip to content

fix(evmrpc): return pruned errors from debug_trace* at unavailable heights (PLT-975) - #3888

Closed
amir-deris wants to merge 8 commits into
mainfrom
amir/plt-975-fix-debug-trace-issues
Closed

fix(evmrpc): return pruned errors from debug_trace* at unavailable heights (PLT-975)#3888
amir-deris wants to merge 8 commits into
mainfrom
amir/plt-975-fix-debug-trace-issues

Conversation

@amir-deris

@amir-deris amir-deris commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes PLT-975 (PR 1 of 2). Historical debug_trace* reads block, receipt, and state stores with independent retention, but only block retention was checked before tracing. That mismatch caused:

Missing data Before
Block pruned Clean error
Receipts pruned Silent [] (HTTP 200)
State pruned Panic → -32603

This PR adds a unified trace guard at the RPC choke point so pruned heights return explicit errors — consistent with eth_getBlockTransactionCountByNumber and the evmrpc/AGENTS.md historical-consistency invariant.

Key changes:

  • Add EnsureTraceHeightAvailable (block + parent block + receipt + parent state) and EnsureStateHeightAvailable on WatermarkManager
  • Run trace availability checks before semaphore acquisition on all debug_trace* entry points (TraceTransaction, TraceBlockBy*, TraceCall, TraceStateAccess, TraceTransactionProfile)
  • Reorder TraceBlockByHash / TraceCall so guard precedes prepareTraceContext
  • Close tx-hash guard hole on litt receipt nodes: propagate ErrReceiptPruned instead of skipping checks when receipt lookup fails
  • Add ErrReceiptPruned sentinel in litt receipt store (distinct from ErrNotFound; wraps ErrNotFound so existing eth_* null-on-not-found handling still applies)
  • Fix blockTraceCacheGet treating empty tx list as a cache hit
  • Nil-guard AsTransaction() in filterTransactions
  • Split the guard for debug_traceCall from replay tracing: add EnsureTraceCallHeightAvailable (block + state only, no receipts) and guardTraceCallRequest* variants, since TraceCall reads state at the requested height directly and never touches receipts — unlike replay tracing, which reads receipts and replays from the parent (height-1) state
  • Fix a latest-tag guard vs. execution mismatch: Backend.BlockByNumber guarded one height (from the ad-hoc ConvertBlockNumber resolution of latest/safe/finalized/earliest) but executed against another. Replaced it with the shared getBlockNumber helper already used by the rest of evmrpc so the guarded height and the executed height are always the same
  • Nil-guard the state store in EnsureTraceCallHeightAvailable / EnsureTraceHeightAvailable: when SS is disabled, trace replay reads state via SC (ctxProvider), not SS retention, so the guard now short-circuits instead of evaluating watermarks against a nil store

Scope / limitations:

  • Receipt backend: ErrReceiptPruned and the retention-floor check live in littReceiptStore only. The pebble receipt backend (receiptBackendPebble) prunes via its own KeepRecent loop but receiptStore.GetReceipt / GetReceiptFromStore enforce no floor and return plain ErrNotFound. On pebble nodes, guardTraceRequestByTxHash still falls through to the latest-height lookback when a pruned receipt is missing. Production nodes with external pruning use litt (pebble + ExternalPruning is rejected at startup); pebble is a legacy/dev path. Lifting the floor check into the shared receipt layer is out of scope for this PR.
  • Litt lazy expiry: the floor check only fires while the receipt is still physically present. After litt deletes it, lookups return ErrNotFound and the tx-hash path falls back to the lookback guard — very old pruned txs may still get "not found" rather than "pruned".

Follow-up (separate future PR): go-ethereum trace_timeout fix for full concurrency relief (PLT-975 PR 2).

Test plan

  • Unit: EnsureTraceHeightAvailable / EnsureStateHeightAvailable / EnsureTraceCallHeightAvailable watermark cases, including SS-disabled (nil state store)
  • Unit: trace guard runs before semaphore; pruned height returns error (not concurrency limit)
  • Unit: receipt-floor boundary (target height guarded; parent height-1 not re-checked in BlockByNumber)
  • Unit: block-retention floor boundary (replay guard rejects height at block earliest when parent block is pruned)
  • Unit: blockTraceCacheGet empty-list false-hit regression
  • Unit: litt receipt store returns ErrReceiptPruned below retention floor
  • Unit: debug_traceCall guarded against block+state only (no receipt check) at both current and historical heights
  • Unit: Backend.BlockByNumber resolves latest/safe/finalized/earliest via the same path used for guarding, so guard and execution heights agree

…ights (PLT-975)

Guard all trace endpoints against block, receipt, and state retention before
acquiring the trace semaphore so pruned heights fail fast with explicit
errors instead of silent empty results or internal panics.

Co-authored-by: Cursor <cursoragent@cursor.com>
@amir-deris amir-deris self-assigned this Aug 10, 2026
@amir-deris amir-deris changed the title fix(evmrpc): return pruned errors from debug_trace* at unavailable he… fix(evmrpc): return pruned errors from debug_trace* at unavailable heights (PLT-975) Aug 10, 2026
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedAug 12, 2026, 12:32 PM

@amir-deris
amir-deris marked this pull request as ready for review August 10, 2026 15:31
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes historical debug_trace* availability and error behavior across RPC entry points and receipt lookups. Risk is moderate because it hardens pruning handling rather than altering auth or consensus paths.

Overview
Historical debug_trace* now fails fast with explicit pruned/unavailable errors instead of returning empty results or panicking when receipt or state retention lags block retention.

Adds EnsureTraceHeightAvailable (block + parent block + receipts + parent state) for replay traces, and a separate EnsureTraceCallHeightAvailable (block + state only) for debug_traceCall. All debug_trace* entry points run these guards before semaphore acquisition.

Introduces ErrReceiptPruned in the litt receipt store so tx-hash guards can distinguish pruned receipts from not-found. Also aligns latest/safe/finalized resolution in Backend.BlockByNumber with the shared getBlockNumber path so guard and execution heights match, and fixes empty-tx-list cache false hits plus a nil AsTransaction() guard.

Reviewed by Cursor Bugbot for commit 25f0527. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread evmrpc/watermark_manager.go
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.76471% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.34%. Comparing base (4814e63) to head (25f0527).

Files with missing lines Patch % Lines
evmrpc/tracers.go 60.00% 16 Missing and 10 partials ⚠️
evmrpc/watermark_manager.go 76.00% 3 Missing and 3 partials ⚠️
evmrpc/simulate.go 50.00% 1 Missing and 1 partial ⚠️
evmrpc/utils.go 0.00% 1 Missing and 1 partial ⚠️
sei-db/ledger_db/receipt/litt_receipt_store.go 60.00% 1 Missing and 1 partial ⚠️
evmrpc/trace_profile.go 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3888      +/-   ##
==========================================
- Coverage   59.45%   58.34%   -1.12%     
==========================================
  Files        2321     2226      -95     
  Lines      198345   186753   -11592     
==========================================
- Hits       117931   108955    -8976     
+ Misses      69213    67508    -1705     
+ Partials    11201    10290     -911     
Flag Coverage Δ
sei-chain-pr 70.32% <60.78%> (?)
sei-db 70.41% <ø> (ø)
sei-db-state-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sei-db/ledger_db/receipt/receipt_store.go 67.00% <ø> (ø)
evmrpc/trace_profile.go 65.93% <0.00%> (ø)
evmrpc/simulate.go 76.34% <50.00%> (-0.12%) ⬇️
evmrpc/utils.go 73.97% <0.00%> (-0.69%) ⬇️
sei-db/ledger_db/receipt/litt_receipt_store.go 68.51% <60.00%> (-0.28%) ⬇️
evmrpc/watermark_manager.go 84.72% <76.00%> (-1.84%) ⬇️
evmrpc/tracers.go 71.26% <60.00%> (+1.56%) ⬆️

... and 161 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

seidroid[bot]
seidroid Bot previously requested changes Aug 10, 2026

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unified trace guard is the right shape and closes real holes (silent [] on pruned receipts, the state-pruned panic, the tx-hash bypass), but three issues block merge: latest-tag traces can now fail transiently because the guard compares the app tip against a lagging watermark, the state leg checks height where replay needs height-1, and the new ErrReceiptPruned sentinel bypasses the "not found" checks in eth_getTransactionReceipt/eth_getTransactionByHash/eth_getBlockReceipts. Codex's point about debug_traceCall not needing receipts is included; Cursor produced no output.

Findings: 3 blocking | 13 non-blocking | 10 posted inline

Blockers

  • None at the file/PR level.
  • 3 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • Cursor's second-opinion pass (cursor-review.md) is empty — no output from that reviewer. Codex's single finding (traceCall does not need receipts) is included below.
  • SS-disabled nodes: Watermarks sets stateEarliest = latest when stateStore == nil, so the new EnsureStateHeightAvailable leg makes EnsureTraceHeightAvailable reject every height below the tip. On a node with state store disabled, all historical debug_trace* now return "has been pruned". The new unit test (nil state store uses latest as earliest from Watermarks) pins this, so it looks intentional and consistent with ResolveHeight/eth_call — but it is a user-visible narrowing that deserves a line in the PR description / release notes.
  • The retention floor that produces ErrReceiptPruned only exists in littReceiptStore. The non-litt receiptStore (sei-db/ledger_db/receipt/receipt_store.go) enforces no floor, so the "tx-hash guard hole" is only closed on the litt backend; on the other backend debug_traceTransaction for a pruned tx still falls through to the latest-height lookback check. Worth stating explicitly (or asserting the litt store is the only production path).
  • Nit: evmrpc/tracers.go now imports the package as receipt, but two functions in the same file declare local variables named receipt (tryTraceCache area, isPanicOrSyntheticTx). It compiles, but evmrpc/tx.go already aliases this package as receiptpkg; matching that avoids a shadowing trap for the next edit.
  • No test covers the new error path in guardTraceRequestByHash (unknown hash now returns block %s not found / the underlying watermark error instead of nil). That is a user-visible change for debug_traceBlockByHash and debug_traceCall-by-hash and is currently unasserted.
  • Test plan's Tier-2 item (docker localnet with aggressive min-retain-blocks) is still unchecked — that is the one check that would have surfaced the latest-tag and parent-state boundary issues below.
  • 7 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread evmrpc/tracers.go
Comment thread evmrpc/watermark_manager.go Outdated
Comment thread sei-db/ledger_db/receipt/receipt_store.go Outdated
Comment thread evmrpc/tracers.go Outdated
// EnsureTraceHeightAvailable verifies block, receipt, and state availability
// for debug_trace* endpoints. All three stores must retain the height.
func (m *WatermarkManager) EnsureTraceHeightAvailable(ctx context.Context, height int64) error {
if err := m.EnsureBlockHeightAvailable(ctx, height); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] EnsureTraceHeightAvailable resolves watermarks twice (EnsureBlockHeightAvailable and EnsureStateHeightAvailable each call Watermarks, each of which does a tmClient.Status). On the by-hash path blockByHashRespectingWatermarks adds a third. Since the guard now runs before the semaphore, that is 3 Status calls per request under unbounded concurrency.

Call Watermarks(ctx) once and run the three ensureWithinWatermarks/floor comparisons against that snapshot — it is also more correct, since the current version can mix watermarks from two different reads.

Comment thread evmrpc/tracers.go
if returnErr = api.validateTraceTracer(config); returnErr != nil {
return nil, returnErr
}
if returnErr = api.guardTraceRequestByHash(ctx, "debug_traceBlockByHash", hash); returnErr != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This reverses the invariant that the deleted TestHashBasedTraceEndpointsAcquireSemaphoreBeforeHashLookup (with its panicHashLookupClient) existed to pin: no Tendermint hash lookup before the semaphore is acquired. Two consequences worth stating explicitly rather than leaving implicit in a test rename:

  1. BlockByHash + up to 3 Status calls now run outside MaxConcurrentTraceCalls, so that knob no longer bounds the pre-trace work an attacker can drive with debug_traceBlockByHash.
  2. The guard now runs on the raw request context, so it is no longer bounded by traceTimeout (prepareTraceContext is what creates that deadline).

Guard-before-wait is the right call for the pruned-height case, so I'm not asking to revert it — but please record the trade-off in the PR body/commit, and consider whether the pre-semaphore lookup needs its own bound.

Comment thread evmrpc/tracers.go
if err == nil {
return receipt, nil
}
if errors.Is(err, ErrReceiptPruned) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] This early return is placed ahead of the legacyReceiptFromKVStore fallback, so a below-floor hit no longer consults the legacy KV store. In practice MigrateLegacyReceiptsBatch deletes the legacy key after writing to litt, so the fallback is usually already dead for these hashes — but that makes the interaction worth a word in the comment, and it is the mechanism behind the legacy-receipt amplifier noted on ErrReceiptPruned.

Comment thread evmrpc/utils.go
continue
}
ethtx, _ := m.AsTransaction()
if ethtx == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The nil guard is the right fix for the panic, but it leans on the discarded error one line up. Prefer making the failure explicit — ethtx, err := m.AsTransaction(); if err != nil || ethtx == nil { continue } — so a malformed EVM message is skipped for a stated reason rather than via a nil that reads as accidental.

Comment thread evmrpc/tracers.go
// blockTraceCacheGet assembles a per-tx hit; returns (nil, false) if any miss.
func blockTraceCacheGet(cache *keeper.TraceDB, height int64, txHashes []common.Hash, config *tracers.TraceConfig) ([]*tracers.TxTraceResult, bool) {
if cache == nil {
if cache == nil || len(txHashes) == 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Correct fix for the false hit, with a side effect worth noting: a genuinely empty block (no EVM txs) can now never be served from this cache and pays a full trace on every request. tryBlockResultCache still covers it if a whole-block entry was baked, so this is likely acceptable — just confirm empty blocks do get block-level entries, otherwise this is a small permanent regression on a common case.

- Resolve latest/pending/safe/finalized trace tags via the watermark's
  safe latest instead of the raw app tip, so debug_trace* no longer
  intermittently errors while receipts/state lag the tip.
- Check the parent height (height-1) against state retention, matching
  how initializeBlock actually replays a traced block.
- Wrap ErrReceiptPruned around ErrNotFound so eth_getTransactionReceipt
  and friends keep returning null for pruned receipts instead of an
  RPC error, while trace guards can still react to it specifically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread evmrpc/watermark_manager.go
Comment thread evmrpc/tracers.go
seidroid[bot]
seidroid Bot previously requested changes Aug 10, 2026

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unified trace guard is the right shape (single choke point, checked before semaphore acquisition), but the state-availability leg treats a disabled state store as "everything below the tip is pruned", which rejects essentially all debug_trace* requests on SS-disabled nodes and appears to contradict the PR's own TestGuardTraceRequestByHashUsesTendermintHeight assertion. Several smaller issues: an unmatched sentinel-less "block not found" error, a skipped legacy-receipt fallback, redundant Watermarks recomputation, and the residual tx-hash gap Codex flagged.

Findings: 3 blocking | 13 non-blocking | 9 posted inline

Blockers

  • evmrpc/tests and evmrpc unit tests could not be executed in this environment, so the failure predicted for TestGuardTraceRequestByHashUsesTendermintHeight (see inline comments on evmrpc/watermark_manager.go and evmrpc/historical_debug_trace_test.go) is from reading the code rather than a run. Please confirm go test ./evmrpc/... ./sei-db/ledger_db/receipt/... is green before merging — if it is, that means the nil-stateStore path behaves differently than I read it and the analysis should be rechecked rather than dismissed.
  • 2 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • Cursor's second-opinion file (cursor-review.md) is empty — that review pass produced no output, so this synthesis reflects only Claude's and Codex's findings.
  • The guard now runs before prepareTraceContext, so the block-by-hash lookup plus up to three Watermarks computations (each an tmClient.Status call + store version reads) happen outside the trace semaphore on every debug_trace* request. The deleted TestHashBasedTraceEndpointsAcquireSemaphoreBeforeHashLookup existed to pin the opposite ordering; the reversal is intentional and justified here, but the concurrency-bounding property it protected is now gone. Worth a note in the PR description (or a cheap pre-check) so the next person doesn't re-reverse it.
  • The pruned-receipt signal only exists in the litt backend. receiptStore.GetReceipt (sei-db/ledger_db/receipt/receipt_store.go:203) has no retention-floor check at all and can only ever return ErrNotFound, so on nodes using that backend the tx-hash guard hole this PR closes stays fully open. Either state that asymmetry in the commit/PR body or lift the floor check into the shared layer.
  • No test covers latestTraceHeight's fallback branches (nil backend/watermarks, or LatestHeight returning an error), nor guardTraceRequestByHash propagating an unknown-hash error, nor the reordering on debug_traceCall specifically (only TraceBlockByHash and TraceBlockByNumber got before-semaphore tests). These are the paths the PR actually changed from lenient to strict.
  • evmrpc/AGENTS.md documents debug_trace* semantics (faithful replay, tracer gating) but not the new availability invariant. Adding a line — "all three of block/receipt/state must retain the height; the guard runs before semaphore acquisition" — would keep the module guide the source of truth for this contract, per the repo's nested-guide convention.
  • Drive-by scope: the filterTransactions nil-guard and the blockTraceCacheGet empty-list change are unrelated to pruning. They're small and defensible, but calling them out as separate concerns in the PR body (or splitting them) would make the pruning change easier to revert in isolation.
  • 7 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread evmrpc/watermark_manager.go Outdated
Comment thread evmrpc/historical_debug_trace_test.go
Comment thread evmrpc/tracers.go
return err
}
return api.guardHistoricalDebugTraceHeight(ctx, endpoint, block.Block.Height)
if block == nil || block.Block == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This branch is unreachable, and the error it returns isn't matchable. blockByHashRespectingWatermarksblockByHashWithRetry already converts blockRes.Block == nil into ErrBlockNotFoundByHash (evmrpc/utils.go:179), so a (nil-block, nil-error) return can't occur.

More importantly, if it ever did, a bare fmt.Errorf("block %s not found") can't be recognised by callers — the rest of the package keys off the ErrBlockNotFoundByHash sentinel (e.g. blockByHashOrNullForJSONRPC maps it to JSON null). Either drop the branch or return fmt.Errorf("block %s: %w", hash.Hex(), ErrBlockNotFoundByHash).

Comment thread evmrpc/tracers.go
if err == nil {
return receipt, nil
}
if errors.Is(err, ErrReceiptPruned) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This early return also skips the legacyReceiptFromKVStore fallback below. Previously a below-floor receipt returned ErrNotFound and the legacy KV store was still consulted; now it short-circuits. The overlap case (a receipt present in litt and below the litt floor and also in legacy KV) should be rare — pre-litt receipts aren't in litt at all — but the ordering change is silent. A brief comment stating that a pruned litt entry is authoritative and deliberately does not fall through to legacy would pin the intent.

Comment thread sei-db/ledger_db/receipt/litt_receipt_store.go

// EnsureTraceHeightAvailable verifies block, receipt, and state availability
// for debug_trace* endpoints. All three stores must retain the height.
func (m *WatermarkManager) EnsureTraceHeightAvailable(ctx context.Context, height int64) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] A single guarded trace call now recomputes Watermarks up to three times: once via latestTraceHeightLatestHeight, once in EnsureBlockHeightAvailable, once in EnsureStateHeightAvailable. Each does a tmClient.Status round trip plus receipt/state version reads, and — since the guard was deliberately moved ahead of the semaphore — this is now unbounded under trace load.

Fetching once and applying the three comparisons inline would be both cheaper and more consistent (today the block and state legs can observe different latest values mid-flight):

blockEarliest, stateEarliest, latest, err := m.Watermarks(ctx)
if err != nil { return err }
if err := ensureWithinWatermarks(height, blockEarliest, latest); err != nil { return err }
if err := m.EnsureReceiptHeightAvailable(height); err != nil { return err }
// ... state leg against stateEarliest/latest

This also reads better against the "guard at the choke point" rule in AGENTS.md: one watermark snapshot, three named checks.

Comment thread evmrpc/tracers.go
// blockTraceCacheGet assembles a per-tx hit; returns (nil, false) if any miss.
func blockTraceCacheGet(cache *keeper.TraceDB, height int64, txHashes []common.Hash, config *tracers.TraceConfig) ([]*tracers.TxTraceResult, bool) {
if cache == nil {
if cache == nil || len(txHashes) == 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Worth a comment on why an empty tx list must not be a hit. As written this reads like a perf regression — an empty block used to short-circuit to [] and now walks the full trace path to produce the same []. The actual reason is that the old code reported a hit without ever consulting the cache, which let an unavailable height answer [] with HTTP 200 (the silent-[] row in your PR table). Naming that in the comment stops someone reverting it as a pointless miss.

Comment thread evmrpc/utils.go
continue
}
ethtx, _ := m.AsTransaction()
if ethtx == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The guard fixes a real nil-deref, but pairing it with the discarded error on the line above makes a malformed message vanish with no trace. Since you're already touching this, consider ethtx, err := m.AsTransaction(); if err != nil || ethtx == nil { continue } — same control flow, but the error is at least available to log or count if this ever fires.

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unified trace-availability guard is well-targeted and the guard/execution height alignment (replacing ConvertBlockNumber with the shared getBlockNumber) is a genuine correctness improvement, backed by focused unit tests. No blocking defects found; the notes below are gaps in the fix's reach (pebble receipt backend, parent-block retention, legacy receipt fallback), a deliberately reversed concurrency invariant that isn't called out, and duplication that the repo's structural guidelines would push back on.

Findings: 0 blocking | 13 non-blocking | 9 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • The Cursor second-opinion pass produced no output (cursor-review.md is empty), so this review is Claude + Codex only.
  • Codex flagged as High that EnsureTraceCallHeightAvailable/latestTraceHeight inherit the receipt-capped latest from Watermarks() even though debug_traceCall needs no receipts. Keeping it for visibility, but I disagree on severity/novelty: StateAndHeaderByNumberOrHashgetBlockByNumberOrHashblockByNumberRespectingWatermarks already applied the same latest cap before this PR, and latest-tag resolution goes through the same wm.LatestHeight, so the guard and the executed height agree and no request is newly rejected. If a receipts-independent safe-latest is wanted for state-only endpoints, that is a separate change to Watermarks().
  • Test coverage stops at the unit boundary for the two behaviours the description headlines. There is no test that guardTraceRequestByTxHash actually propagates ErrReceiptPruned out of debug_traceTransaction/debug_traceStateAccess (only the litt store-level test at littidx_test.go), and none covering the "state pruned → panic → -32603" case the summary table lists as fixed — the state leg is exercised only through WatermarkManager directly.
  • The Tier 2 item in the test plan (docker localnet with aggressive min-retain-blocks, comparing trace errors against the eth_getBlockTransactionCountByNumber control) is still unchecked. Given the fix is specifically about behaviour at retention floors, that is the check most likely to surface the parent-height and backend-coverage gaps noted inline.
  • 9 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread evmrpc/tracers.go
if returnErr = api.validateTraceTracer(config); returnErr != nil {
return nil, returnErr
}
if returnErr = api.guardTraceRequestByHash(ctx, "debug_traceBlockByHash", hash); returnErr != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This reverses an invariant that was previously pinned on purpose. The deleted TestHashBasedTraceEndpointsAcquireSemaphoreBeforeHashLookup used a client whose BlockByHash panicked with "hash lookup should not happen before trace context setup", i.e. hash resolution was deliberately deferred until after the MaxConcurrentTraceCalls semaphore. After this change every debug_traceBlockByHash / debug_traceCall (and, via latestTraceHeight, every by-number trace) performs a tmClient.Status plus a Tendermint BlockByHash lookup outside the concurrency limit.

The trade-off looks defensible — a pruned request shouldn't have to win a semaphore slot to learn it's pruned, and these are cheap in-process reads next to an actual trace — but the PR description frames it only as "reorder so guard precedes prepareTraceContext" and doesn't mention that a pinned protection was removed. Worth stating the reasoning explicitly here or in the description, since the next reader will find the deleted test in history and not know it was intentional.

Comment thread evmrpc/watermark_manager.go
Comment thread sei-db/ledger_db/receipt/receipt_store.go
if err == nil {
return receipt, nil
}
if errors.Is(err, ErrReceiptPruned) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This early return skips the legacy KV fallback below. Previously a below-floor hit surfaced as ErrNotFound from GetReceiptFromStore and fell through to legacyReceiptFromKVStore; now it errors out immediately. For a node whose legacy KV store still holds receipts for a height that litt has aged past, that's a served receipt turning into an error.

The overlap is probably empty in practice (legacy receipts predate litt, so they shouldn't have litt entries at all), which is why I'm not calling it blocking — but the safer ordering is to attempt legacyReceiptFromKVStore first and only return the ErrReceiptPruned wrap if that also misses. That keeps "pruned" meaning "unavailable everywhere", which is what the trace guard actually wants to assert.

if err := m.EnsureBlockHeightAvailable(ctx, height); err != nil {
return err
}
if m.stateStore == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The stateStore == nil short-circuit and its comment are duplicated verbatim in EnsureTraceHeightAvailable (line 221). AGENTS.md's "guard at the choke point, never at each caller" applies: a third trace guard added later has to remember this, and the identical comment in two places is the tell. Hoisting it into one named helper — something like ensureReplayStateAvailable(ctx, height) whose doc comment carries the why (SS disabled ⇒ replay reads state via SC/ctxProvider, so SS watermarks don't apply) — would leave both Ensure* methods reading as a clean sequence of steps.

Related: EnsureStateHeightAvailable is exported and, with stateStore == nil, Watermarks() sets stateEarliest = latest, so it reports every historical height as pruned — pinned by TestEnsureStateHeightAvailable's "nil state store" subtest. That's a trap for a future caller who reaches for it directly. Worth a doc-comment sentence saying it reports SS retention only and is not meaningful when SS is disabled.

Comment thread evmrpc/tracers.go
func (api *DebugAPI) guardHistoricalDebugTraceByTxHash(ctx context.Context, endpoint string, hash common.Hash) error {
if api.keeper == nil {
return nil
func (api *DebugAPI) guardTraceRequest(ctx context.Context, endpoint string, height int64) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Two things about the new guard layer:

  1. guardTraceRequest{,ByNumber,ByHash,ByNumberOrHash,ByTxHash} and guardTraceCallRequest{,ByNumber,ByHash,ByNumberOrHash} are nine functions where the by-number/by-hash/by-number-or-hash trios are byte-for-byte identical apart from which Ensure* method the leaf calls. Threading the availability check through instead — e.g. one family taking ensure func(context.Context, int64) error, with EnsureTraceHeightAvailable / EnsureTraceCallHeightAvailable passed at the entry points — would halve this without losing the replay-vs-call distinction the PR is careful to draw.

  2. Ordering side effect: the watermark check now runs before guardHistoricalDebugTraceHeight, so recordHistoricalDebugTraceAttempt no longer fires for a height that is both pruned and beyond maxBlockLookback. If that metric is used to size MaxTraceLookbackBlocks, it now undercounts on pruning-heavy nodes. Probably fine, but it's a silent observability change not mentioned in the description.

Comment thread evmrpc/tracers.go
// blockTraceCacheGet assembles a per-tx hit; returns (nil, false) if any miss.
func blockTraceCacheGet(cache *keeper.TraceDB, height int64, txHashes []common.Hash, config *tracers.TraceConfig) ([]*tracers.TxTraceResult, bool) {
if cache == nil {
if cache == nil || len(txHashes) == 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The what is clear but the why is the load-bearing part and it's only in the test name. The reason an empty txHashes must not be a hit is that Backend.BlockByNumber drops txs whose receipts aren't found (if !found { continue }), so an empty list is indistinguishable from "receipts pruned" — which is exactly the silent []/HTTP 200 failure mode in the PR's table. Per AGENTS.md, that belongs in the doc comment above (currently just "assembles a per-tx hit; returns (nil, false) if any miss").

Also worth a word that a genuinely empty block now always falls through to the full trace path; harmless (no txs to replay, and tryBlockResultCache still covers the block-level entry), but it reads like an oversight without the note.

Comment thread evmrpc/simulate.go
func (b Backend) BlockByNumber(ctx context.Context, bn rpc.BlockNumber) (*ethtypes.Block, []tracersutils.TraceBlockMetadata, error) {
blockNum := b.ConvertBlockNumber(bn)
tmBlock, err := blockByNumberRespectingWatermarks(ctx, b.tmClient, b.watermarks, &blockNum, 1)
blockNumberPtr, err := getBlockNumber(ctx, b.tmClient, bn)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Swapping ConvertBlockNumber for getBlockNumber also changes pending handling: the old code did panic("tracing on pending block is not supported"), while getBlockNumber maps PendingBlockNumber to nil (= latest). resolveDebugTraceBlockNumber maps it to latestTraceHeight too, so guard and execution stay consistent and this is a strict improvement over panicking — but it's a user-visible semantic change (debug_traceBlockByNumber("pending") now traces latest instead of erroring) that the description doesn't list. Worth a line in the PR body, since it's the kind of thing an integrator notices before we do.

require.NoError(t, wm.EnsureTraceCallHeightAvailable(t.Context(), 175))

// Receipts pruned below 150; replay guard fails, TraceCall guard does not check receipts.
rs.earliest = 150

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] rs.earliest = 150 is a no-op — rs is constructed with earliest: 150 five lines up. The comment above ("Receipts pruned below 150") reads as though this line establishes that state, so a later reader may adjust the constructor and assume this line re-pins it. Either drop the assignment or initialise rs with earliest: 1 so the mutation is what actually moves the floor (as TestEnsureTraceHeightAvailable at line 192 does).

@seidroid
seidroid Bot dismissed stale reviews from themself August 11, 2026 13:57

Superseded: latest AI review found no blocking issues.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a558ab6. Configure here.

Comment thread evmrpc/tracers.go
switch number {
case rpc.SafeBlockNumber, rpc.FinalizedBlockNumber, rpc.LatestBlockNumber, rpc.PendingBlockNumber:
return api.ctxProvider(LatestCtxHeight).BlockHeight(), nil
return api.latestTraceHeight(ctx), nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lookback uses tip, not safe latest

Medium Severity

resolveDebugTraceBlockNumber now resolves latest via latestTraceHeight (watermark-safe), but guardHistoricalDebugTraceHeight still measures lookback against the raw app tip. When tip outruns the safe latest and maxBlockLookback is small (especially 0), the safe height is treated as historical and common debug_trace* requests fail even though watermark checks would allow them.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a558ab6. Configure here.

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid, well-tested fix: the trace guards are consolidated at the RPC choke point, the block/receipt/state retention mismatch is closed, and Backend.BlockByNumber now guards and executes against the same resolved height. No confirmed correctness blockers; the remaining notes are gaps in the guard's coverage (post-GC receipts, receipt-capped safe-latest), a deliberate but undocumented reversal of the "semaphore before hash lookup" invariant, and some redundant watermark reads.

Findings: 0 blocking | 11 non-blocking | 7 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Cursor's second-opinion review file (cursor-review.md) is empty — that pass produced no output, so this review reflects Claude + Codex only.
  • The PR deletes TestHashBasedTraceEndpointsAcquireSemaphoreBeforeHashLookup and its panicHashLookupClient, which encoded a deliberate invariant: hash lookups must happen after semaphore acquisition. debug_traceBlockByHash and debug_traceCall-by-hash now perform a Tendermint block-by-hash read (plus 1–2 Status() calls) on every request before the concurrency gate can reject it. That is the right trade for correct pruned errors, but it re-opens the unbounded pre-gate load path the deleted test was protecting — worth stating explicitly in the PR description, and ideally a comment at the guard call sites so the ordering isn't "fixed" back later.
  • Backend.BlockByNumber for pending changes from panic("tracing on pending block is not supported") to tracing the latest block (getBlockNumber maps pending → nil → safe latest). This matches evmrpc/AGENTS.md ("Sei ... will treat [pending] equivalent to final/safe/latest") and removing an RPC-path panic is an improvement, but TestConvertBlockNumber was deleted without a replacement case pinning the new pending semantics.
  • Consider a debug_trace* entry in evmrpc/AGENTS.md documenting the new error contract: pruned block / pruned receipts / pruned state now return explicit errors rather than [], null, or -32603. This is a client-visible behavioral contract and the file already documents debug_* deviations.
  • 7 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread evmrpc/tracers.go Outdated
Comment thread evmrpc/tracers.go
}

func (api *DebugAPI) guardTraceRequestByHash(ctx context.Context, endpoint string, hash common.Hash) error {
if api.backend == nil || api.tmClient == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This checks api.backend == nil || api.tmClient == nil but not api.backend.watermarks == nil, unlike guardTraceRequest (line 106) and guardTraceCallRequest (line 174), which both guard the nil watermark manager explicitly. If watermarks is nil, blockByHashRespectingWatermarks returns errNoHeightSource outright (watermark_manager.go:267), so debug_traceBlockByHash hard-fails with "unable to determine height information" where the old code skipped the guard and proceeded.

Same applies to guardTraceCallRequestByHash at line 191. Production always wires a non-nil manager, so this is robustness/consistency rather than a live bug — but the file's own convention is to nil-check it, and doing so here keeps the two hash guards degrading the same way as the height guards.

Comment thread evmrpc/tracers.go
return err
}
} else if rcpt != nil {
return api.guardTraceRequest(ctx, endpoint, int64(rcpt.BlockNumber)) //nolint:gosec

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Worth double-checking the freshly-landed-tx path: the receipt's exact BlockNumber is fed to EnsureTraceHeightAvailable, whose EnsureBlockHeightAvailable leg compares against the composite safe latest = min(tmLatest, ctxTip, receiptStore.LatestVersion(), stateStore.GetLatestVersion()). The receipt being readable implies the first three are at or above that height, but the SS latest can lag. If it does, tracing a tx in the newest block now returns requested height N is not yet available — a transient rejection on the hottest trace path.

latestTraceHeight (line 132) exists precisely to absorb that skew, but it only covers the latest/safe/finalized tag path, not this one where the height comes from the receipt. If SS commit is synchronous with the receipt write this is a non-issue; if it isn't, consider clamping to the safe latest (or treating ErrBlockHeightNotYetAvailable as non-fatal) for a receipt-confirmed height.


// EnsureTraceCallHeightAvailable verifies block and state availability for
// debug_traceCall. TraceCall loads state at the requested height via
// StateAndHeaderByNumberOrHash and never reads receipts.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] "never reads receipts" overstates what this guard achieves, and Codex flags the same thing. The receipt floor is indeed skipped, but EnsureBlockHeightAvailable derives latest from Watermarks, where latest = min(tmLatest, ctxTip, m.receiptStore.LatestVersion(), ...). So a receipt store lagging at the tip still rejects an explicit height whose block and state are both present, and still drags the latest tag to an older block via latestTraceHeight.

Using the shared safe-latest is consistent with the rest of evmrpc (ResolveHeight, blockByNumberRespectingWatermarks) and is probably the intended conservative choice — but the doc comment reads as a stronger guarantee than the code gives. Suggest narrowing it to something like "skips the receipt retention floor; the shared safe-latest ceiling still applies."

// EnsureTraceHeightAvailable verifies block, receipt, and state availability
// for debug_trace* replay endpoints (transaction/block). Replay loads parent
// state (height-1) and reads receipts at the requested height.
func (m *WatermarkManager) EnsureTraceHeightAvailable(ctx context.Context, height int64) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] EnsureTraceHeightAvailable calls Watermarks twice (once via EnsureBlockHeightAvailable, once via EnsureStateHeightAvailable), and callers reaching it through guardTraceRequestByNumberlatestTraceHeight add a third; guardTraceRequestByHash adds a fourth through blockByHashRespectingWatermarks. Each call is a tmClient.Status() plus store version loads, and they can observe different snapshots mid-flight, so the block and state legs aren't necessarily checked against the same latest.

Resolving blockEarliest, stateEarliest, latest once here and calling ensureWithinWatermarks twice against that snapshot would be both cheaper and internally consistent.

if err == nil {
return receipt, nil
}
if errors.Is(err, ErrReceiptPruned) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This early return skips the legacyReceiptFromKVStore fallback that a below-floor receipt previously reached (it used to return ErrNotFound and fall through at line 250). During a migration window where a receipt exists in both litt and the legacy KV store, a litt retention floor above that block will now hide a receipt the legacy store can still serve.

That may well be intended — pruned is pruned, and answering from legacy would make the retention floor a lie. But it is an unstated behavior change in a shared store method used well beyond the trace guards; worth a one-line comment recording the choice.

Comment thread evmrpc/utils.go
continue
}
ethtx, _ := m.AsTransaction()
if ethtx == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The nil check fixes the panic, but the error from AsTransaction() is still discarded on line 231, so a message that fails to decode is now silently dropped from block results instead of crashing. Per the Consistency section of evmrpc/AGENTS.md ("RPC responses for historical heights should never change"), a transaction quietly vanishing from eth_getBlockByNumber / tx counts is the kind of thing you want a trace of. Capturing the error and logging it at warn would make the drop diagnosable without changing the response.

@amir-deris

Copy link
Copy Markdown
Contributor Author

Will create a new PR to cleanup AI threads.

@amir-deris amir-deris closed this Aug 12, 2026

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid, well-tested consolidation of the debug_trace* availability guards, and the latest-tag guard/execution mismatch fix in Backend.BlockByNumber is a genuine improvement. One blocking regression: the new parent-block guard floors at 0 instead of the chain's initial height, so tracing the chain's first block (debug_traceBlockByNumber("earliest") / height 1) now fails on full-history nodes.

Findings: 1 blocking | 10 non-blocking | 4 posted inline

Blockers

  • None at the file/PR level.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • cursor-review.md is empty — the Cursor second-opinion pass produced no output, so this review reflects only the Claude and Codex passes.
  • Codex flagged (P1) that EnsureTraceCallHeightAvailable inherits a latest that Watermarks caps by receiptStore.LatestVersion(), so debug_traceCall can reject a height on receipt-store lag despite never reading receipts. I'd keep this as a note rather than a bug: that same capped latest governs eth_call/eth_getBalance at explicit heights, so debug_traceCall rejecting there is consistent with the rest of evmrpc, and the latest/safe/finalized tags resolve through latestTraceHeight so the common path is unaffected. Worth a comment on EnsureTraceCallHeightAvailable recording the deliberate reuse.
  • A single debug_trace* request now recomputes watermarks several times: resolveDebugTraceBlockNumberlatestTraceHeightWatermarks, then EnsureBlockHeightAvailable, ensureReplayParentBlockAvailable, and EnsureStateHeightAvailable each call Watermarks again — four to five tmClient.Status() calls plus store version reads per request. Besides the cost, the legs are non-atomic: a prune landing between them can produce an inconsistent verdict. Computing the tuple once in EnsureTrace*HeightAvailable and passing it to the individual checks would fix both.
  • guardTraceRequest* and guardTraceCallRequest* are four near-identical function pairs differing only in which availability check runs. Per AGENTS.md ("guard at the choke point"), the dispatch (by-number / by-hash / by-number-or-hash / latest fallback) is the invariant worth having in one place — parameterize it with the availability func rather than duplicating the whole family. As written, a future trace endpoint has to remember which of the eight to call.
  • On SS-disabled nodes both trace guards short-circuit the state leg entirely, so historical state pruned out of SC (IAVL) still reaches the replay path — the original panic → -32603 failure mode the PR is fixing remains on that configuration. The PR describes the nil-store short-circuit but not that it leaves this case unguarded; worth stating explicitly alongside the pebble-backend limitation already in the description.
  • TestHashBasedTraceEndpointsAcquireSemaphoreBeforeHashLookup is deleted, and with it the invariant it pinned: hash lookups must not happen before semaphore acquisition. Moving the guard ahead of prepareTraceContext deliberately inverts that, so every debug_traceBlockByHash / debug_traceCall-by-hash request now performs a Tendermint hash lookup and a Status() call before any concurrency limit applies. The trade-off looks right (pruned heights should not queue behind the semaphore), but it is a real change in DoS posture and deserves a note in the PR description or a comment at the new guard call sites, since the deleted test was the only record of the prior intent.
  • TestEnsureTraceCallHeightAvailable (watermark_manager_test.go) sets rs.earliest = 150 when rs was already constructed with earliest: 150 — the redundant assignment reads as if it is establishing the precondition.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.

// ensureReplayParentBlockAvailable verifies the parent block height replay
// tracing loads for validator set lookup in initializeBlock.
func (m *WatermarkManager) ensureReplayParentBlockAvailable(ctx context.Context, height int64) error {
parentBlockHeight := max(height-1, 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocker] Flooring the parent at 0 makes tracing the chain's first block unconditionally fail. For height == genesisInitialHeight (1 on a normal chain) this computes parentBlockHeight = 0, and EnsureBlockHeightAvailable(0) runs ensureWithinWatermarks(0, blockEarliest, latest) with blockEarliest = 1, returning requested height 0 has been pruned; earliest available is 1.

So on a full-history node debug_traceBlockByNumber("earliest") — which resolveDebugTraceBlockNumber explicitly resolves to Genesis.InitialHeight — and debug_traceBlockByNumber(0x1) both now error out where they previously worked. The actual replay does not need a parent block: initializeBlock only calls tmClient.Validators(ctx, &prevBlockHeight, ...), and Tendermint treats height 0 as "latest", so the call it is guarding would have succeeded.

The state leg two lines above already handles this correctly with max(height-1, m.genesisInitialHeight()); the same floor belongs here:

parentBlockHeight := max(height-1, m.genesisInitialHeight())

The guard is right for the pruned/state-synced case (tracing at blockEarliest on a snapshot node genuinely cannot load blockEarliest-1 validators) — it is only the genesis edge that over-rejects. Note TestEnsureTraceHeightAvailableParentBlockFloor currently pins the over-strict behaviour at the block floor, so it will need the genesis case distinguished from the pruned case.

if err := m.EnsureBlockHeightAvailable(ctx, height); err != nil {
return err
}
if m.stateStore == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The m.stateStore == nil short-circuit is repeated here and again in EnsureTraceHeightAvailable (line 225), with the same comment both times. AGENTS.md asks for the guard at the choke point rather than at each caller — EnsureStateHeightAvailable is that choke point, and every caller of it needs this branch.

As it stands EnsureStateHeightAvailable has a surprising standalone contract when SS is disabled: Watermarks sets stateEarliest = latest, so any historical height reports "has been pruned" (your own TestEnsureStateHeightAvailable subtest pins exactly that). A third caller added later gets the wrong answer unless they remember to repeat the check. Folding the nil-store case into EnsureStateHeightAvailable — returning nil, with the SC/ctxProvider rationale in its doc comment — makes it an invariant instead of a convention.

Comment thread evmrpc/tracers.go
}

func (api *DebugAPI) guardTraceRequestByHash(ctx context.Context, endpoint string, hash common.Hash) error {
if api.backend == nil || api.tmClient == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This nil-check is narrower than the one in guardTraceRequest, which tests api.backend != nil && api.backend.watermarks != nil. If backend is non-nil but backend.watermarks is nil, blockByHashRespectingWatermarks returns errNoHeightSource ("unable to determine height information"), and since this rewrite propagates errors instead of falling through, debug_traceBlockByHash hard-fails on a node in that state — previously it returned nil and tracing proceeded.

Same in guardTraceCallRequestByHash (line 191). Extending the condition to api.backend == nil || api.backend.watermarks == nil || api.tmClient == nil keeps the degraded-configuration behaviour aligned with guardTraceRequest.

Comment thread evmrpc/utils.go
continue
}
ethtx, _ := m.AsTransaction()
if ethtx == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The equivalent nil-guard added in simulate.go carries // AsTransaction may return nil if it fails to unpack the tx data. — worth repeating here, since ethtx, _ := m.AsTransaction() discards the error and the bare continue gives a reader nothing to go on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant